/**
*    Copyright (C) 2008  Chase Kernan 
*    chase.kernan@gmail.com
*
*    This program is free software: you can redistribute it and/or modify
*    it under the terms of the GNU General Public License as published by
*    the Free Software Foundation, either version 3 of the License, or
*    (at your option) any later version.
*
*    This program is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU General Public License for more details.
*
*    You should have received a copy of the GNU General Public License
*    along with this program.  If not, see <http://www.gnu.org/licenses/>.
* 
*/

package langhx;

/**
    IMPORTANT: If the REMOVE_ENSURE compilation flag is used, then these methods 
    have no effect.
    
    Used to "ensure" that the contract has been upheld. This is generally used
    at the end of a function.
    
    It contains methods that test the value to make sure that certain conditions
    have been upheld. If the condition isn't upheld, [EnsureError] is thrown.
    
    NOTE: The value is returned if the condition is upheld, so it can be
    used like so:
        return Ensure.inRange(r, 0.0, 1.0);
**/
class Ensure {
    
    /**
        Throws [EnsureError] if the value is null or false.
        
        Otherwise returns true.
        
        Since this will throw an error if the value is null, it takes a 
        [Null<Bool>] and returns [Bool].
    **/
    public static inline function isTrue(value : Null<Bool>) 
                                  : #if !REMOVE_ENSURE Bool 
                                    #else Null<Bool> 
                                    #end {
        #if !REMOVE_ENSURE
        if (value == null || !value) throw new EnsureError(false, "Is true");
        return true;
        
        #else
        return value;
        #end
    }
    
    /**
        Throws [EnsureError] if the value is null or true.
        
        Otherwise this returns false.
        
        Since this will throw an error if the value is null, it takes a 
        [Null<Bool>] and returns [Bool].
    **/
    public static inline function isFalse(value : Null<Bool>) 
                                  : #if !REMOVE_ENSURE Bool 
                                    #else Null<Bool> 
                                    #end {
        #if !REMOVE_ENSURE
        if (value == null || value) throw new EnsureError(true, "Is false");
        return false;
        
        #else
        return value;
        #end
    }
    
    /**
        Throws [EnsureError] if the value is null.
        
        Otherwise it returns the value.
    **/
    public static inline function notNull<T>(value : T) : T {
        #if !REMOVE_ENSURE
        if (value == null) throw new EnsureError(null, "Not null");
        #end
        
        return value;
    }
    
    /**
        Throws [EnsureError] if the value is outside of the given range 
        (inclusive).
        
        Otherwise returns the value.
        
        NOTE: This will only return a float. For integer ranges, use 
        [inIntRange].
        
        Since this will throw an error if the value is null, it takes a 
        [Null<Float>] and returns [Float].
    **/
    public static inline function inRange(value : Null<Float>, lower : Float, 
                                          upper : Float) 
                                  : #if !REMOVE_ENSURE Float 
                                    #else Null<Float> 
                                    #end {
        #if !REMOVE_ENSURE
        if (value == null || value < lower || value > upper) { 
            throw new EnsureError(value, "In range " + lower + " to " + upper);
        }
        
        return untyped value; //it cannot be null anymore
        
        #else
        return value;
        #end
    }
    
    /**
        Throws [EnsureError] if the value is outside of the given range 
        (inclusive).
        
        Otherwise returns the value.
        
        NOTE: This will only return an integer. For [Float] ranges, use 
        [inRange].
        
        Since this will throw an error if the value is null, it takes a 
        [Null<Int>] and returns [Int].
    **/
    public static inline function inIntRange(value : Null<Int>, lower : Int,
                                             upper : Int) 
                                  : #if !REMOVE_ENSURE Int 
                                    #else Null<Int> 
                                    #end {
        #if !REMOVE_ENSURE
        if (value == null || value < lower || value > upper) { 
            throw new EnsureError(value, "In range " + lower + " to " + upper);
        }
        
        return untyped value; //it cannot be null anymore
        
        #else
        return value;
        #end
    }
    
    /**
        Throws [EnsureError] if the given iterable is empty 
        (ie [value.iterator().hasNext() is false) or if the value itself is 
        null.
        
        Otherwise it returns the value.
    **/
    public static inline function notEmpty<T>(value : Iterable<T>) 
                                  : Iterable<T> {
        #if !REMOVE_ENSURE
        if (value == null || !value.iterator().hasNext()) {
            throw new EnsureError(value, "Not Empty");
        }
        #end
        
        return value;
    }
}

/**
    Thrown if one of the ensured conditions isn't met.
**/
class EnsureError {
    
    private var value : Dynamic;
    private var ensured : String;
    
    /**
        Takes the value given, and the condition it didn't meet.
    **/
    public function new(value : Dynamic, ensured : String) {
        this.value = value;
        this.ensured = ensured;
    }
    
    /**
        Returns the value given.
    **/
    public inline function getValue() : Dynamic {
        return value;
    }
    
    /**
        Returns the [String] representation of the condition the value
        failed to meet.
    **/
    public inline function getEnsured() : String {
        return ensured;
    }
    
    /**
        Returns:
            Contract wasn't fufilled.
            [ensured] condition wasn't met. Value given was [value].
    **/
    public function toString() : String {
        return "Contract wasn't fufilled.\n " + 
                ensured + " condition wasn't met. Value given was " + 
                value + ".";
    }
}

#if LANG_HX_UNIT_TEST
import haxe.unit.TestCase;

class EnsureTest extends haxe.unit.TestCase {
    
    public function testNotNull() {
        try {
            Ensure.notNull(null);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        var a = [1, 2, 3];
        assertEquals(a, Ensure.notNull(a));
    }
    
    public function testNotEmpty() {
        try {
            Ensure.notEmpty(new Array<Int>());
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        try {
            Ensure.notEmpty(null);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        var a = [1, 2, 3];
        assertEquals(a, cast Ensure.notEmpty(a));
    }
    
    public function testInRange() {
        try {
            Ensure.inRange(-0.5, 0.0, 1.0);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        try {
            Ensure.inRange(null, 0.0, 1.0);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        assertEquals(0.5, Ensure.inRange(0.5, 0.0, 1.0));
        assertEquals(0.0, Ensure.inRange(0.0, 0.0, 1.0));
        assertEquals(1.0, Ensure.inRange(1.0, 0.0, 1.0));
    }
    
    public function testInIntRange() {
        try {
            Ensure.inIntRange(-5, 0, 10);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        try {
            Ensure.inIntRange(null, 2, 5);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        assertEquals(5, Ensure.inIntRange(5, 0, 10));
        assertEquals(0, Ensure.inIntRange(0, 0, 10));
        assertEquals(10, Ensure.inIntRange(10, 0, 10));
    }
    
    public function testIsTrue() {
        try {
            Ensure.isTrue(false);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        try {
            Ensure.isTrue(null);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        assertTrue(Ensure.isTrue(true));
    }
    
    public function testIsFalse() {
        try {
            Ensure.isFalse(true);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        try {
            Ensure.isFalse(null);
            assertTrue(false);
        } catch (e : EnsureError) {}
        
        assertFalse(Ensure.isFalse(false));
    }
}
#end        
